package org.ayachinene.utils;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.jetbrains.annotations.NotNull;
import org.springframework.util.Assert;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;

import static org.springframework.util.StringUtils.stripFilenameExtension;

/**
 * 将文件切分成几个部分。
 * 切分策略：
 * 1. 按照份数切分
 * 2. 按照每份的大小切分
 */
public class FileSplitter {

    /**
     * 默认输出路径
     */
    public static final String DEFAULT_OUTPUT_DIR_NAME = ".file-splitter";

    /**
     * 要切分的源文件
     */
    private final File file;

    /**
     * 每份的大小，与partNum两者只能选一个，以字节为单位
     */
    private Long partSize;

    /**
     * 切分为多少份，与partSize两者只能选一个
     */
    private Integer partNum;

    /**
     * 执行切分，输出切分结果到源文件所在目录下的默认输出路径，
     * 以[去后缀源文件名]_[index]的形式命名分段文件，若文件已存在，则覆盖旧文件
     * @return 分割结果
     * @see SplitResult
     * @throws IOException when io exception occurred
     */
    public SplitResult split() throws IOException {
        File dir = new File(file.getAbsoluteFile().getParent() + "/" + DEFAULT_OUTPUT_DIR_NAME);
        if (!dir.exists() || dir.isFile()) {
            Assert.isTrue(dir.mkdir(), "Fail to make dir" + dir.getAbsolutePath() + "!!");
        }
        FileUtils.cleanDirectory(dir);
        return splitTo(dir.getAbsolutePath());
    }

    /**
     * 执行切分，输出切分结果到dirPath，以[去后缀源文件名]_[index]的形式命名分段文件，若文件已存在，则覆盖旧文件
     * @param dirPath 输出路径
     * @return 分割结果
     * @see SplitResult
     * @throws IOException when io exception occurred
     */
    public SplitResult splitTo(String dirPath) throws IOException {
        return splitTo(dirPath, stripFilenameExtension(file.getName()));
    }

    /**
     * 执行切分，输出切分结果到dirPath，以[name]_[index]的形式命名分段文件，若文件已存在，则覆盖旧文件
     * @param dirPath 输出路径
     * @param name 输出文件名
     * @see SplitResult
     * @throws IOException when io exception occurred
     */
    public SplitResult splitTo(String dirPath, String name) throws IOException {
        File dir = FileUtils.getFile(dirPath);
        Assert.isTrue(dir.exists() && dir.isDirectory(), "Invalid dir path!!");

        long size = FileUtils.sizeOf(file);

        // 用户指定按份数拆分，否则用户指定按大小拆分
        if (partNum != null) {
            if (partNum == 1)
                partSize = size;
            else
                partSize = size / partNum + 1;
        }

        List<Pair<String, Long>> fileInfos = new ArrayList<>();

        int cnt = 0;
        // 开始拆分
        try (InputStream in = FileUtils.openInputStream(file)) {
            long leftSize = size;
            while (leftSize > 0) {
                File part = FileUtils.getFile(dir, name + "_" + cnt);
                OutputStream out = FileUtils.openOutputStream(part);
                byte[] bytes = in.readNBytes(Math.toIntExact(partSize));
                out.write(bytes);
                out.close();
                Pair<String, Long> fileInfo = new ImmutablePair<>(part.getAbsolutePath(), (long) bytes.length);
                fileInfos.add(fileInfo);
                leftSize -= bytes.length;
                cnt++;
            }
        }

        fileInfos.forEach(info -> {
            String msg = StringUtils.substringAfterLast(info.getKey(), "/") + " " +
                    info.getValue() + "B " +
                    FileUtils.byteCountToDisplaySize(info.getValue());
            System.out.println(msg);
        });

        return new SplitResult(fileInfos);
    }

    private FileSplitter(File file, Long partSize) {
        this.file = file;
        this.partSize = partSize;
    }

    private FileSplitter(File file, Integer partNum) {
        this.file = file;
        this.partNum = partNum;
    }

    public static SplitStrategy filePath(@NotNull String filePath) {
        File file = FileUtils.getFile(filePath);
        Assert.isTrue(file.exists() && file.isFile(), "Invalid file path!!");
        return new SplitStrategy(file);
    }

    static class SplitStrategy {

        private final File file;

        private SplitStrategy(File file) {
            this.file = file;
        }

        public ActualBuilder partSize(long partSize) {
            Assert.isTrue(partSize > 0 && partSize <= FileUtils.sizeOf(file),
                    "Invalid partSize!!");
            return new ActualBuilder(file, partSize);
        }

        public ActualBuilder partNum(int partNum) {
            Assert.isTrue(partNum > 0 && partNum <= FileUtils.sizeOf(file),
                    "Invalid partNum");
            return new ActualBuilder(file, partNum);
        }
    }

    static class ActualBuilder {

        private final File file;
        private Long partSize;
        private Integer partNum;

        private ActualBuilder(File file, Long partSize) {
            this.file = file;
            this.partSize = partSize;
        }

        private ActualBuilder(File file, Integer partNum) {
            this.file = file;
            this.partNum = partNum;
        }

        public FileSplitter build() {
            if (partSize != null)
                return new FileSplitter(file, partSize);
            return new FileSplitter(file, partNum);
        }
    }
}
